﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="ModelBase.cs" company="JDML Consulting Ltd.">
//   Copyright 2011 Photon Project - http://photon.codeplex.com
// </copyright>
// --------------------------------------------------------------------------------------------------------------------

namespace Photon.ComponentModel.Modelling
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using System.ComponentModel.DataAnnotations;
    using System.Linq;
    using System.Linq.Expressions;
    using System.Runtime.Serialization;
    using Collections;
    using Reflection;

#if SILVERLIGHT

    using CommandManager = Windows.Input.Commands.CommandManager;

#else

    using CommandManager = System.Windows.Input.CommandManager;

#endif

    [DataContract]
    public partial class ModelBase : ISuspendNotifyChanged, INotifyPropertyChanged, IDataErrorInfo
    {
        #region Fields

        private static readonly PropertyChangedEventArgs EmptyPropertyChangedEventArgs = new PropertyChangedEventArgs(string.Empty);
        private BitMask _changedProperties;
        private ErrorContainer _errorContainer;
        private int _changedPropertyCount;
        private int _suspectNotifyCount;
        private MetaModelProperty _lastChangedProperty;

        #endregion

        public ModelBase()
        {
            Initialize();
        }

        private ErrorContainer ErrorContainer
        {
            get { return _errorContainer ?? (_errorContainer = new ErrorContainer()); }
        }

        protected MetaModelType MetaModelType { get; private set; }

        public bool HasErrors
        {
            get { return ErrorContainer.HasErrors; }
        }

        #region IDataErrorInfo Members

        string IDataErrorInfo.this[string propertyName]
        {
            get { return ErrorContainer.GetErrors(propertyName).FirstOrDefault(); }
        }

        string IDataErrorInfo.Error
        {
            get
            {
                if (HasErrors)
                {
                    var errors = new List<string>();
                    foreach (var propertyName in MetaModelType.Properties.Select(x => x.Name).Union(new[] { string.Empty }))
                    {
                        foreach (var error in ErrorContainer.GetErrors(propertyName).Where(error => !errors.Contains(error)))
                        {
                            errors.Add(error);
                        }
                    }

                    return String.Join("\r\n", errors);
                }

                return null;
            }
        }

        #endregion

        #region INotifyPropertyChanged Members

        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        #region ISuspendNotifyChanged Members

        public virtual IDisposable NewSuspendNotify()
        {
            return new SuspendNotifyScope(this);
        }

        public bool IsNotifySuspended
        {
            get { return _suspectNotifyCount > 0 || IsDeserializing; }
        }

        #endregion

        public IEnumerable GetErrors(string propertyName)
        {
            return ErrorContainer.GetErrors(propertyName);
        }

        private void Initialize()
        {
            // hook up to type information
            MetaModelType = MetaModel.Default.GetOrCreateType(GetType());

            // allocate change properties bit mask
            _changedProperties = new BitMask(MetaModelType.Properties.Count);
        }

        protected void BeginSuspendNotify()
        {
            _suspectNotifyCount++;
        }

        protected void EndSuspendNotify()
        {
            _suspectNotifyCount--;
            if (!IsNotifySuspended)
            {
                RaiseNotifications();
            }
        }

        private void RaiseNotifications()
        {
            if (_changedPropertyCount != 0)
            {
                var handler = PropertyChanged;
                if (handler != null)
                {
                    switch (this._changedPropertyCount)
                    {
                        case -1:
                            handler(this, EmptyPropertyChangedEventArgs);
                            break;
                        case 1:
                            handler(this, this._lastChangedProperty.PropertyChangedEventArgs);
                            break;
                        default:
                            {
                                var propertyCount = MetaModelType.Properties.Count;
                                for (int i = 0, j = _changedPropertyCount; i < propertyCount && j > 0; i++)
                                {
                                    if (_changedProperties[i])
                                    {
                                        handler(this, MetaModelType.Properties[i].PropertyChangedEventArgs);
                                        j--;
                                    }
                                }
                            }

                            break;
                    }
                }
            }

            _changedProperties.Clear();
            _changedPropertyCount = 0;
            _lastChangedProperty = null;
        }

        protected void AddError(string propertyName, string error)
        {
            var hadErrors = HasErrors;
            if (ErrorContainer.AddError(propertyName, error))
            {
                OnErrorsChanged(MetaModelType.Properties.Find(propertyName));
            }

            if (hadErrors != HasErrors)
            {
                SetPropertyChanged(MetaModelType.Properties["HasErrors"]);
            }
        }

        protected void RemoveError(string propertyName, string error)
        {
            var hadErrors = HasErrors;
            if (ErrorContainer.RemoveError(propertyName, error))
            {
                OnErrorsChanged(MetaModelType.Properties.Find(propertyName));
            }

            if (hadErrors != HasErrors)
            {
                SetPropertyChanged(MetaModelType.Properties["HasErrors"]);
            }
        }

        protected void ClearErrors()
        {
            var hadErrors = HasErrors;

            // ErrorsChanged does't work like PropertyChanged string.Empty doesn't mean any, so cannot must clear each
            foreach (var property in MetaModelType.Properties)
            {
                if (ErrorContainer.ClearErrors(property.Name))
                {
                    OnErrorsChanged(property);
                }
            }

            if (ErrorContainer.ClearErrors(string.Empty))
            {
                OnErrorsChanged(null);
            }

            if (hadErrors != HasErrors)
            {
                SetPropertyChanged(MetaModelType.Properties["HasErrors"]);
            }
        }

        protected bool SetProperty<T>(ref T field, T value, Expression<Func<T>> propertyExpression, 
            PropertyChangedCallback<T> onChanged = null)
        {
            return SetProperty(ref field, value, propertyExpression.ExtractMemberName(), onChanged);
        }

        protected virtual bool SetProperty<T>(ref T field, T value, string propertyName, 
            PropertyChangedCallback<T> propertyChangeCallback = null)
        {
            var changed = !EqualityComparer<T>.Default.Equals(field, value);
            if (changed)
            {
                var property = MetaModelType.Properties.Find(propertyName);
                if (!IsDeserializing)
                {
                    changed = property.OnChanging(this, field, ref value);
                }

                BeginSuspendNotify();
                try
                {
                    if (changed)
                    {
                        var oldValue = field;
                        field = value;

                        SetPropertyChanged(property);
                        if (!IsDeserializing)
                        {
                            ValidatePropertySet(property, value);
                        }

                        if (propertyChangeCallback != null)
                        {
                            propertyChangeCallback(oldValue, field);
                        }

                        OnChanged(propertyName);
                    }
                    else
                    {
                        // mark as changed to ensure controls that do not re-read values are updated.
                        SetPropertyChanged(property);
                    }
                }
                finally
                {
                    EndSuspendNotify();
                }
            }

            return changed;
        }

        private void SetPropertyChanged(MetaModelProperty property)
        {
            if (IsDeserializing)
            {
                return;
            }
            if (IsNotifySuspended)
            {
                if (property == null)
                {
                    _changedPropertyCount = -1;
                }
                else if (_changedPropertyCount != -1)
                {
                    _lastChangedProperty = property;

                    var index = property.Index;
                    if (_changedProperties.Update(index, true))
                    {
                        _changedPropertyCount++;
                    }
                }
            }
            else
            {
                RaisePropertyChanged(property.Name);
            }
        }

        private void ValidatePropertySet<T>(MetaModelProperty property, T value)
        {
            if (!property.HasValidation)
            {
                return;
            }

            var hadErrors = HasErrors;

            var results = new Collection<ValidationResult>();
            property.OnValidating(value, new ValidationContext(this, null, null)
                {
                    MemberName = property.Name, 
                    DisplayName = property.DisplayName
                }, 
                results);
            SetErrors(property, results);
            if (hadErrors != HasErrors)
            {
                SetPropertyChanged(MetaModelType.Properties["HasErrors"]);
            }
        }

        private void SetErrors(MetaModelProperty property, IEnumerable<ValidationResult> results)
        {
            // get property name
            var propertyName = property != null ? property.Name : string.Empty;

            // get old and new errors
            var newErrors = results.Select(r => r.ErrorMessage);
            var oldErrors = ErrorContainer.GetErrors(propertyName);

            // apply the changes
            var changed = newErrors.Except(oldErrors).Aggregate(false, (current, error) =>
                current |
                    ErrorContainer.AddError(propertyName, error)) |
                        oldErrors.Except(newErrors).Aggregate(false, (current, error) =>
                            current |
                                ErrorContainer.RemoveError(propertyName, error));

            if (changed)
            {
                OnErrorsChanged(property);
            }
        }

        protected virtual void OnChanged(string propertyName)
        {
#if SILVERLIGHT
            CommandManager.InvalidateRequerySuggested();
#endif
        }

        /// <summary>
        /// Raises the <see cref="PropertyChanged"/> event.
        /// </summary>
        /// <param name="propertyName">Name of the property.</param>
        /// <remarks>
        /// Is notifications have been suspended the change event will not 
        /// be raised until until notifications are resumed.
        /// </remarks>
        public void RaisePropertyChanged(string propertyName)
        {
            var property = !string.IsNullOrEmpty(propertyName) ? MetaModelType.Properties[propertyName] : null;
            if (!IsNotifySuspended)
            {
                var handler = PropertyChanged;
                if (handler != null)
                {
                    handler(this, property != null ? property.PropertyChangedEventArgs : EmptyPropertyChangedEventArgs);
                }
            }
            else
            {
                SetPropertyChanged(property);
            }
        }

        /// <summary>
        /// Raises the property changed event.
        /// </summary>
        /// <typeparam name="TProperty">The type of the property.</typeparam>
        /// <param name="propertyExpression">An expression representing the property to raise a changed event for.</param>
        public void RaisePropertyChanged<TProperty>(Expression<Func<TProperty>> propertyExpression)
        {
            RaisePropertyChanged(propertyExpression.ExtractMemberName());
        }

        /// <summary>
        /// Validates this instance.
        /// </summary>
        /// <returns><c>true</c> if the object is valid; otherwise, <c>false</c>.</returns>
        /// <remarks>
        /// Applies any validation specified through custom attributes of type <see cref="ValidationAttribute"/> and then invokes 
        /// the <see cref="OnValidate"/> method. Errors added within the <see cref="OnValidate"/> must be explicitly removed when 
        /// they no longer apply.
        /// <example>
        /// <code>
        /// public override void OnValidate()
        /// {
        ///     const string message = "Exactly one of MutuallyExclusiveValueX or MutuallyExclusiveValueY must be set.";
        ///     if (String.IsNullOrEmpty(MutuallyExclusiveValueX) == String.IsNullOrEmpty(MutuallyExclusiveValueY))
        ///     {
        ///         AddError("MutuallyExclusiveValueX", message);
        ///         AddError("MutuallyExclusiveValueY", message);
        ///     }
        ///     else
        ///     {
        ///         // it is important to remove errors that no longer apply
        ///         RemoveError("MutuallyExclusiveValueX", message);
        ///         RemoveError("MutuallyExclusiveValueY", message);
        ///     }
        /// }
        /// </code>
        /// </example>
        /// </remarks>
        public bool Validate()
        {
            BeginSuspendNotify();
            try
            {
                // track changes to HasErrors
                var hadErrors = HasErrors;

                // validate the entire object
                var results = new Collection<ValidationResult>();
                Validator.TryValidateObject(this, new ValidationContext(this, null, null), results, true);

                // apply property errors
                foreach (var property in MetaModelType.Properties)
                {
                    var currentProperty = property;
                    SetErrors(property, 
                        results.Where(x => x.MemberNames.Any(name => name == currentProperty.Name)));
                }

                // apply object errors
                SetErrors(null, results.Where(x => !x.MemberNames.Any()));

                // allow descendents to perform any custom validation
                OnValidate();

                // did HasErrors change?
                if (hadErrors != HasErrors)
                {
                    SetPropertyChanged(MetaModelType.Properties["HasErrors"]);
                }
            }
            finally
            {
                EndSuspendNotify();
            }

            return !HasErrors;
        }

        protected virtual void OnValidate()
        {
        }

        private void OnErrorsChanged(MetaModelProperty property)
        {
            RaiseErrorsChanged(property);
            CommandManager.InvalidateRequerySuggested();
        }

        partial void RaiseErrorsChanged(MetaModelProperty property);

        #region Serialization

        protected bool IsDeserializing { get; private set; }

        [OnDeserializingAttribute]
#if SILVERLIGHT
        public
#else
        private
#endif
            void OnDeserializing(StreamingContext context)
        {
            if (IsDeserializing)
            {
                throw new InvalidOperationException();
            }

            Initialize();
            IsDeserializing = true;
        }

        [OnDeserializedAttribute]
#if SILVERLIGHT
        public
#else
        private
#endif
            void OnDeserialized(StreamingContext context)
        {
            if (!IsDeserializing)
            {
                throw new InvalidOperationException();
            }

            IsDeserializing = false;
        }

        #endregion

        #region Nested type: SuspendNotifyScope

        protected class SuspendNotifyScope : IDisposable
        {
            #region Fields

            private ModelBase _model;

            #endregion

            public SuspendNotifyScope(ModelBase model)
            {
                _model = model;
                _model.BeginSuspendNotify();
            }

            #region IDisposable Members

            public void Dispose()
            {
                if (_model != null)
                {
                    // clear model (exception raised in PropertyChange shouldn't break the mechanism)
                    var model = _model;
                    _model = null;

                    // decrement notify
                    model.EndSuspendNotify();
                }
            }

            #endregion
        }

        #endregion
    }

    internal struct BitMask
    {
        #region Fields

        private readonly byte[] _data;
        private readonly int _count;

        #endregion

        public BitMask(int size)
        {
            // validate size
            _count = Guard.ArgumentLessThan("size", size, 0);

            // allocate data storage
            _data = new byte[(size + 7) / 8];
        }

        public int Count
        {
            get { return _count; }
        }

        /// <summary>
        /// Updates the <paramref name="value"/> at the specified <paramref name="index"/>.
        /// </summary>
        /// <param name="index">The index.</param>
        /// <param name="value">if set to <c>true</c> [value].</param>
        /// <returns><c>true</c> if the <paramref name="value"/> at the specified <paramref name="index"/> was updated; otherwise <c>false</c>.</returns>
        public bool Update(int index, bool value)
        {
            if (index < 0 || index >= Count)
            {
                throw CollectionExceptions.IndexOutOfRange(index);
            }

            var mask = 1 << (index % 8);
            index /= 8;
            if (((this._data[index] & mask) == mask) == value)
            {
                return false;
            }

            if (value)
            {
                this._data[index] |= (byte)mask;
            }
            else
            {
                this._data[index] &= (byte)(~mask);
            }

            return true;
        }

        public bool this[int index]
        {
            get
            {
                if (this._data == null)
                {
                    throw CollectionExceptions.IndexOutOfRange(index);
                }

                return (this._data[index / 8] & (1 << (index % 8))) != 0;
            }

            set
            {
                if (index < 0 || index >= Count)
                {
                    throw CollectionExceptions.IndexOutOfRange(index);
                }

                if (value)
                {
                    _data[index / 8] |= (byte)(1 << (index % 8));
                }
                else
                {
                    _data[index / 8] &= (byte)(~(1 << (index % 08)));
                }
            }
        }

        public void Clear()
        {
            Array.Clear(_data, 0, _data.Length);
        }

        public byte[] Data
        {
            get { return _data; }
        }
    }
}